/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package com.googlecode.bluetools.dom4j.io;

import java.util.List;

import org.w3c.dom.DOMImplementation;

import com.googlecode.bluetools.dom4j.Attribute;
import com.googlecode.bluetools.dom4j.CDATA;
import com.googlecode.bluetools.dom4j.Comment;
import com.googlecode.bluetools.dom4j.Document;
import com.googlecode.bluetools.dom4j.DocumentException;
import com.googlecode.bluetools.dom4j.Element;
import com.googlecode.bluetools.dom4j.Entity;
import com.googlecode.bluetools.dom4j.Namespace;
import com.googlecode.bluetools.dom4j.ProcessingInstruction;
import com.googlecode.bluetools.dom4j.Text;
import com.googlecode.bluetools.dom4j.tree.NamespaceStack;

/**
 * <p>
 * <code>DOMWriter</code> takes a DOM4J tree and outputs it as a W3C DOM object
 * </p>
 * 
 * @author <a href="mailto:james.strachan@metastuff.com">James Strachan </a>
 * @version $Revision: 1.1 $
 */
public class DOMWriter {
	private static boolean loggedWarning = false;

	private static final String[] DEFAULT_DOM_DOCUMENT_CLASSES = {
			"org.apache.xerces.dom.DocumentImpl", // Xerces
			"gnu.xml.dom.DomDocument", // GNU JAXP
			"org.apache.crimson.tree.XmlDocument", // Crimson
			"com.sun.xml.tree.XmlDocument", // Sun's Project X
			"oracle.xml.parser.v2.XMLDocument", // Oracle V2
			"oracle.xml.parser.XMLDocument", // Oracle V1
			"com.bluebamboo.dom4j.dom.DOMDocument" // Internal DOM implementation
	};

	// the Class used to create new DOM Document instances
	private Class domDocumentClass;

	/** stack of <code>Namespace</code> objects */
	private NamespaceStack namespaceStack = new NamespaceStack();

	public DOMWriter() {
	}

	public DOMWriter(Class domDocumentClass) {
		this.domDocumentClass = domDocumentClass;
	}

	public Class getDomDocumentClass() throws DocumentException {
		Class result = domDocumentClass;

		if (result == null) {
			// lets try and find one in the classpath
			int size = DEFAULT_DOM_DOCUMENT_CLASSES.length;

			for (int i = 0; i < size; i++) {
				try {
					String name = DEFAULT_DOM_DOCUMENT_CLASSES[i];
					result = Class.forName(name, true, DOMWriter.class.getClassLoader());

					if (result != null) {
						break;
					}
				}
				catch (Exception e) {
					// could not load class correctly
					// lets carry on to the next one
				}
			}
		}

		return result;
	}

	/**
	 * Sets the DOM {@link org.w3c.dom.Document}implementation class used by the writer when creating DOM documents.
	 * 
	 * @param domDocumentClass is the Class implementing the {@linkorg.w3c.dom.Document} interface
	 */
	public void setDomDocumentClass(Class domDocumentClass) {
		this.domDocumentClass = domDocumentClass;
	}

	/**
	 * Sets the DOM {@link org.w3c.dom.Document}implementation class name used by the writer when creating DOM
	 * documents.
	 * 
	 * @param name is the name of the Class implementing the {@link org.w3c.dom.Document} interface
	 * 
	 * @throws DocumentException if the class could not be loaded
	 */
	public void setDomDocumentClassName(String name) throws DocumentException {
		try {
			this.domDocumentClass = Class.forName(name, true, DOMWriter.class.getClassLoader());
		}
		catch (Exception e) {
			throw new DocumentException("Could not load the DOM Document " + "class: " + name, e);
		}
	}

	public org.w3c.dom.Document write(Document document) throws DocumentException {
		if (document instanceof org.w3c.dom.Document) {
			return (org.w3c.dom.Document) document;
		}

		resetNamespaceStack();

		org.w3c.dom.Document domDocument = createDomDocument(document);
		appendDOMTree(domDocument, domDocument, document.content());
		namespaceStack.clear();

		return domDocument;
	}

	public org.w3c.dom.Document write(Document document, org.w3c.dom.DOMImplementation domImpl)
			throws DocumentException {
		if (document instanceof org.w3c.dom.Document) {
			return (org.w3c.dom.Document) document;
		}

		resetNamespaceStack();

		org.w3c.dom.Document domDocument = createDomDocument(document, domImpl);
		appendDOMTree(domDocument, domDocument, document.content());
		namespaceStack.clear();

		return domDocument;
	}

	protected void appendDOMTree(org.w3c.dom.Document domDocument, org.w3c.dom.Node domCurrent, List content) {
		int size = content.size();

		for (int i = 0; i < size; i++) {
			Object object = content.get(i);

			if (object instanceof Element) {
				appendDOMTree(domDocument, domCurrent, (Element) object);
			}
			else if (object instanceof String) {
				appendDOMTree(domDocument, domCurrent, (String) object);
			}
			else if (object instanceof Text) {
				Text text = (Text) object;
				appendDOMTree(domDocument, domCurrent, text.getText());
			}
			else if (object instanceof CDATA) {
				appendDOMTree(domDocument, domCurrent, (CDATA) object);
			}
			else if (object instanceof Comment) {
				appendDOMTree(domDocument, domCurrent, (Comment) object);
			}
			else if (object instanceof Entity) {
				appendDOMTree(domDocument, domCurrent, (Entity) object);
			}
			else if (object instanceof ProcessingInstruction) {
				appendDOMTree(domDocument, domCurrent, (ProcessingInstruction) object);
			}
		}
	}

	protected void appendDOMTree(org.w3c.dom.Document domDocument, org.w3c.dom.Node domCurrent, Element element) {
		String elUri = element.getNamespaceURI();
		String elName = element.getQualifiedName();
		org.w3c.dom.Element domElement = domDocument.createElementNS(elUri, elName);

		int stackSize = namespaceStack.size();

		// add the namespace of the element first
		Namespace elementNamespace = element.getNamespace();

		if (isNamespaceDeclaration(elementNamespace)) {
			namespaceStack.push(elementNamespace);
			writeNamespace(domElement, elementNamespace);
		}

		// add the additional declared namespaces
		List declaredNamespaces = element.declaredNamespaces();

		for (int i = 0, size = declaredNamespaces.size(); i < size; i++) {
			Namespace namespace = (Namespace) declaredNamespaces.get(i);

			if (isNamespaceDeclaration(namespace)) {
				namespaceStack.push(namespace);
				writeNamespace(domElement, namespace);
			}
		}

		// add the attributes
		for (int i = 0, size = element.attributeCount(); i < size; i++) {
			Attribute attribute = (Attribute) element.attribute(i);
			String attUri = attribute.getNamespaceURI();
			String attName = attribute.getQualifiedName();
			String value = attribute.getValue();
			domElement.setAttributeNS(attUri, attName, value);
		}

		// add content
		appendDOMTree(domDocument, domElement, element.content());

		domCurrent.appendChild(domElement);

		while (namespaceStack.size() > stackSize) {
			namespaceStack.pop();
		}
	}

	protected void appendDOMTree(org.w3c.dom.Document domDocument, org.w3c.dom.Node domCurrent, CDATA cdata) {
		org.w3c.dom.CDATASection domCDATA = domDocument.createCDATASection(cdata.getText());
		domCurrent.appendChild(domCDATA);
	}

	protected void appendDOMTree(org.w3c.dom.Document domDocument, org.w3c.dom.Node domCurrent, Comment comment) {
		org.w3c.dom.Comment domComment = domDocument.createComment(comment.getText());
		domCurrent.appendChild(domComment);
	}

	protected void appendDOMTree(org.w3c.dom.Document domDocument, org.w3c.dom.Node domCurrent, String text) {
		org.w3c.dom.Text domText = domDocument.createTextNode(text);
		domCurrent.appendChild(domText);
	}

	protected void appendDOMTree(org.w3c.dom.Document domDocument, org.w3c.dom.Node domCurrent, Entity entity) {
		org.w3c.dom.EntityReference domEntity = domDocument.createEntityReference(entity.getName());
		domCurrent.appendChild(domEntity);
	}

	protected void appendDOMTree(org.w3c.dom.Document domDoc, org.w3c.dom.Node domCurrent, ProcessingInstruction pi) {
		org.w3c.dom.ProcessingInstruction domPI = domDoc.createProcessingInstruction(pi.getTarget(), pi.getText());
		domCurrent.appendChild(domPI);
	}

	protected void writeNamespace(org.w3c.dom.Element domElement, Namespace namespace) {
		String attributeName = attributeNameForNamespace(namespace);

		// domElement.setAttributeNS("", attributeName, namespace.getURI());
		domElement.setAttribute(attributeName, namespace.getURI());
	}

	protected String attributeNameForNamespace(Namespace namespace) {
		String xmlns = "xmlns";
		String prefix = namespace.getPrefix();

		if (prefix.length() > 0) {
			return xmlns + ":" + prefix;
		}

		return xmlns;
	}

	protected org.w3c.dom.Document createDomDocument(Document document) throws DocumentException {
		org.w3c.dom.Document result = null;

		// use the given domDocumentClass (if not null)
		if (domDocumentClass != null) {
			try {
				result = (org.w3c.dom.Document) domDocumentClass.newInstance();
			}
			catch (Exception e) {
				throw new DocumentException("Could not instantiate an instance " + "of DOM Document with class: "
						+ domDocumentClass.getName(), e);
			}
		}
		else {
			// lets try JAXP first before using the hardcoded default parsers
			result = createDomDocumentViaJAXP();

			if (result == null) {
				Class theClass = getDomDocumentClass();

				try {
					result = (org.w3c.dom.Document) theClass.newInstance();
				}
				catch (Exception e) {
					throw new DocumentException("Could not instantiate an " + "instance of DOM Document "
							+ "with class: " + theClass.getName(), e);
				}
			}
		}

		return result;
	}

	protected org.w3c.dom.Document createDomDocumentViaJAXP() throws DocumentException {
		try {
			return JAXPHelper.createDocument(false, true);
		}
		catch (Throwable e) {
			if (!loggedWarning) {
				loggedWarning = true;

				if (SAXHelper.isVerboseErrorReporting()) {
					// log all exceptions as warnings and carry
					// on as we have a default SAX parser we can use
					System.out.println("Warning: Caught exception attempting " + "to use JAXP to create a W3C DOM "
							+ "document");
					System.out.println("Warning: Exception was: " + e);
					e.printStackTrace();
				}
				else {
					System.out.println("Warning: Error occurred using JAXP to " + "create a DOM document.");
				}
			}
		}

		return null;
	}

	protected org.w3c.dom.Document createDomDocument(Document document, DOMImplementation domImpl)
			throws DocumentException {
		String namespaceURI = null;
		String qualifiedName = null;
		org.w3c.dom.DocumentType docType = null;

		return domImpl.createDocument(namespaceURI, qualifiedName, docType);
	}

	protected boolean isNamespaceDeclaration(Namespace ns) {
		if ((ns != null) && (ns != Namespace.NO_NAMESPACE) && (ns != Namespace.XML_NAMESPACE)) {
			String uri = ns.getURI();

			if ((uri != null) && (uri.length() > 0)) {
				if (!namespaceStack.contains(ns)) {
					return true;
				}
			}
		}

		return false;
	}

	protected void resetNamespaceStack() {
		namespaceStack.clear();
		namespaceStack.push(Namespace.XML_NAMESPACE);
	}
}

/*
 * Redistribution and use of this software and associated documentation ("Software"), with or without modification, are
 * permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain copyright statements and notices. Redistributions must also contain a
 * copy of this document.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 * following disclaimer in the documentation and/or other materials provided with the distribution.
 * 
 * 3. The name "DOM4J" must not be used to endorse or promote products derived from this Software without prior written
 * permission of MetaStuff, Ltd. For written permission, please contact dom4j-info@metastuff.com.
 * 
 * 4. Products derived from this Software may not be called "DOM4J" nor may "DOM4J" appear in their names without prior
 * written permission of MetaStuff, Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
 * 
 * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
 * 
 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 */
